Chomu's Blog.

>

Posts

GitHub

[포스코x코딩온] 웹 풀스택 과정 7기 6주차 화요일 회고

목차

TypeScript

과정에서 배우지는 않았지만 내용을 따라하면서 TypeScript를 사용해보았다.
그런데 무작정 사용하려고 하니 오류가 무더기로 발생했다.
이를 해결하기 위한 고군분투의 과정을 기록해둔다.

*.ts 파일 실행 오류

당연히 node가 *.ts 파일을 실행해 줄거라고 생각했는데 전혀 그렇지 않았다.
*.ts 파일을 실행하려면 몇 가지 과정이 필요했다.
먼저 typescriptts-node를 설치했다.
그리고 프로젝트 루트 폴더에서 tsc --init 명령어로 tsconfig.json 파일을 생성했다.
이후 ts-node*.ts 파일을 실행시킬 수 있었다.

npm i typescript ts-node
npx tsc --init
npx ts-node src/index.ts

절대경로

매번 상대경로를 쓰다보니 import 경로가 너무 길어졌다.
그런데 React를 공부하다보니 절대경로를 사용하는 방법이 있어서 찾아보았다.
tsconfig.json 파일에 다음과 같은 설정을 추가하면 된다.

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["*"]
    }
  }
}

이 설정을 추가하면 @/로 시작하는 경로를 절대경로로 사용할 수 있다.
다만 Node는 절대경로를 지원하지 않기 때문에 tsconfig-paths 라는 라이브러리를 사용해야 한다.
나는 다음 명령어를 index.sh 파일에 추가해두고 사용했다.

ts-node -r tsconfig-paths/register --files index.ts

Sequelize

Sequelize는 Node.js에서 SQL을 사용하기 위한 ORM(Object-Relational Mapping) 라이브러리이다.
npx sequelize init 명령어를 통해 Sequelize를 초기화할 수 있다.
해당 명령어를 실행하면 여러가지 파일들이 생성된다.
이후 models 폴더 안에 사용하고 싶은 테이블에 맞춰 다음과 같은 파일을 생성한다.

import { DataTypes, Sequelize } from "sequelize";
 
export default function TableName(sequelize: Sequelize, dataTypes: typeof DataTypes) {
  return sequelize.define("TableName", {
    /*
    열이름: {
      속성: 속성값
    }
    */
    id: {
      type: dataTypes.INTEGER, // 타입
      autoIncrement: true, // 자동 증가
      primaryKey: true, // 기본키
      allowNull: false // null 허용 여부
    },
    name: {
      type: dataTypes.STRING(20),
    }
    // ...
  }, {
    tableName: "TableName", // 테이블 이름
    freezeTableName: true, // 테이블 이름 고정 (false 일 경우 테이블 이름을 복수로 만들어버림)
    timestamps: false, // createdAt, updatedAt 컬럼 생성 여부
  });
}

이후 다음과 같이 사용할 수 있다.

import db from "@/models";
async function get(req: Request, res: Response) {
  const row = await db.TableName.findByPk(/* pk */); // 기본키 값으로 조회
  const rows = await db.TableName.findAll(); // 전체 조회
  const rows = await db.TableName.findAll({where: {/* 조건 */},}); // 조건에 맞는 행 조회
  const row = await db.TableName.create({/* 데이터 */}); // 데이터 생성
  await db.TableName.update({/* 수정할 데이터 */}, {where: {/* 조건 */},}); // 조건에 맞는 데이터 수정
  await db.TableName.destroy({where: {/* 조건 */},}); // 조건에 맞는 데이터 삭제
}

models/index.js 뜯어보기

Sequelize 초기화 시 생성되는 파일 중 models/index.js 라는 파일이 있다.
해당 파일을 import 하면 DB 객체를 얻을 수 있다.
그러나 내 프로젝트에서는 해당 파일에서 오류가 나왔다.
오류를 고치는 김에 TS로 작성하기 위해 직접 뜯어 봤다.

'use strict';
 
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const process = require('process');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
// config.json 파일을 불러와서 현재 환경에 맞는 설정을 가져옴
const db = {};
 
let sequelize; // config 파일에 맞춰 sequelize 객체 생성
if (config.use_env_variable) {
  sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
  sequelize = new Sequelize(config.database, config.username, config.password, config);
}
 
fs
  .readdirSync(__dirname)
  .filter(file => {
    return (
      // models 폴더 안에 있는 파일들 중 index.js, *.test.js 파일은 제외한 모든 .js 파일을 불러옴
      file.indexOf('.') !== 0 &&
      file !== basename &&
      file.slice(-3) === '.js' &&
      file.indexOf('.test.js') === -1
    );
  })
  .forEach(file => {
    // 각 파일들을 불러와서 해당 파일의 default 함수에 sequelize, DataTypes 객체를 넘겨줌
    const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
    // 그 값을 db 객체에 넣어줌
    db[model.name] = model;
  });
 
Object.keys(db).forEach(modelName => {
  // db 객체에 있는 모델들을 순회하면서 associate 메소드가 있는 경우 이를 실행
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});
 
db.sequelize = sequelize; // db 객체에 sequelize 객체를 넣어줌
db.Sequelize = Sequelize;
 
module.exports = db; // db 객체를 export

먼저 나는 "type": "module"를 지정했기 때문에 requireimport로 바꿔야 했다.
그렇게 되면 중간의 forEach 문에서 폴더 내 파일들을 불러올 수 없었다.
그래서 그냥 수동으로 불러왔다.

// models/index.ts
import { Sequelize, DataTypes } from "sequelize";
import process from "process";
import Visitor from "./visitor";
...
const visitor = Visitor(sequelize, DataTypes);
...

config.json 파일도 config/index.ts 파일로 바꿔서 불러왔다.

//  config/index.ts
import { Dialect } from "sequelize";
 
interface Config {
  username: string;
  password: string;
  database: string;
  host: string;
  dialect: Dialect;
}
 
interface Configs {
  [key: string]: Config;
}
 
const configs: Configs = {
  development: {
    username: "u1",
    password: "1",
    database: "exerdb",
    host: "localhost",
    dialect: "mysql",
  },
  // test: {},
  // production: {},
}
 
export default configs;
// models/index.ts
...
import configs from "@/config";
...

이를 수정했더니 fs, path 모듈은 필요 없어져서 제거했다.
또 TS를 적용하니 sequelize 객체를 생성할 때 오류가 발생했다. 따라서 envVariable 변수를 따로 빼줬다.

...
const { database, username, password, use_env_variable, ...config } = configs[env];
const envVariable = use_env_variable && process.env[use_env_variable]
const sequelize = envVariable
  ? new Sequelize(envVariable, config)
  : new Sequelize(database, username, password, config);
...

마지막으로 DB의 타입을 정의하고 export 해줬다.

...
interface DB {
  sequelize: Sequelize;
  Sequelize: typeof Sequelize;
  visitor: typeof visitor;
}
 
export default <DB>{
  sequelize,
  Sequelize,
  visitor,
};

최종 코드는 다음과 같다.

import { Sequelize, DataTypes } from "sequelize";
import process from "process";
import configs from "@/config";
import Visitor from "./visitor";
 
 
const env = process.env.NODE_ENV || "development";
const { database, username, password, use_env_variable, ...config } = configs[env];
const envVariable = use_env_variable && process.env[use_env_variable]
const sequelize = envVariable
  ? new Sequelize(envVariable, config)
  : new Sequelize(database, username, password, config);
 
const visitor = Visitor(sequelize, DataTypes);
 
interface DB {
  sequelize: Sequelize;
  Sequelize: typeof Sequelize;
  visitor: typeof visitor;
}
 
export default <DB>{
  sequelize,
  Sequelize,
  visitor,
};

암호화

보안 문제는 어디서나 일어날 수 있는데 이는 서버 상에서도 마찬가지이다.
따라서 서버에서도 보안을 위해 암호화를 해야 한다.
Node 에서는 이를 위해 crypto 모듈을 제공한다.

crypto

import crypto from "crypto";
 
function createSaltHash(pw: string) {
  const salt = crypto.randomBytes(8).toString("hex");
  const hash = crypto.pbkdf2Sync(pw, salt, 100000, 64, "sha512").toString("hex");
  return { salt, hash };
}
 
function comparePassword(pw: string, salt: string, hash: string) {
  const hashedPW = crypto.pbkdf2Sync(pw, salt, 100000, 64, "sha512").toString("hex");
  return hashedPW === hash;
}

createSaltHash 함수를 통해 salt 값과 pw의 해시값을 생성한 뒤 유저의 정보로 저장한다.
이후 비밀번호를 확인할 때 comparePassword 함수를 통해 비밀번호가 일치하는지 확인한다.

bcrypt

bcrypt는 외부 암호화 라이브러리이다.
crypto 보다 쉽게 사용할 수 있다는 장점이 있다.

import bcrypt from "bcrypt";
 
async function createSaltHash(pw: string) {
  const salt = await bcrypt.genSalt(8);
  const hash = await bcrypt.hash(pw, salt);
  return { salt, hash };
}
 
async function comparePassword(pw: string, salt: string, hash: string) {
  const hashedPW = await bcrypt.hash(pw, salt);
  return hashedPW === hash;
}

메소드 이름 뒤에 Sync를 붙이면 동기함수로 사용할 수 있다.
하지만 공식문서는 비동기 함수를 권장한다.
이벤트 루프를 막아 앱의 성능을 저하시킬 수 있기 때문이다.